feat: add frontend subscription success recovery - #11286
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughIntroduces a pending subscription checkout recovery system that persists attempted checkouts in localStorage with 6-hour expiry, recovers them on visibility/auth changes, validates completed checkouts against pending records, and enriches subscription success telemetry with checkout metadata. Updates billing portal accessor to return a success boolean and removes polling-based confirmation from the subscription UI. Changes
Sequence DiagramsequenceDiagram
actor User
participant Client as Client App
participant Storage as localStorage
participant Server as Billing Server
participant Telemetry as Telemetry
User->>Client: Initiates checkout (subscribe/change)
Client->>Server: Open billing portal
Server-->>Client: Portal window opened/failed
alt Portal opened successfully
Client->>Storage: Record pending checkout attempt<br/>(tier, cycle, type, previous_tier)
Storage-->>Client: Attempt persisted
else Portal failed
Client->>Client: Return early, no storage
end
Note over Client,Telemetry: Background recovery flow
Client->>Client: Page visibility/storage/auth event
Client->>Storage: Check for pending attempt
Storage-->>Client: Found pending checkout
Client->>Client: Scheduled retry with backoff
Client->>Server: Re-fetch subscription status
Server-->>Client: Updated status + subscription tier
Client->>Storage: Validate: does status tier match pending tier?
alt Tier matches (checkout succeeded)
Storage-->>Client: Retrieve attempt metadata
Client->>Telemetry: Track subscription success<br/>(with checkout_id, value, currency, ecommerce)
Telemetry-->>Client: Event recorded
Client->>Storage: Clear pending attempt
Storage-->>Client: Cleared
else Tier mismatch or still pending
Client->>Client: Reschedule recovery retry
end
Client->>User: Update UI subscription state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 1135 passed, 0 failed · 2 flaky📊 Browser Reports
|
📦 Bundle: 5.16 MB gzip 🔴 +1.43 kBDetailsSummary
Category Glance App Entry Points — 22.3 kB (baseline 22.3 kB) • 🔴 +1 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.22 MB (baseline 1.22 MB) • 🔴 +1 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 76.6 kB (baseline 76.6 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 482 kB (baseline 482 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.1 kB (baseline 17.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 109 kB (baseline 109 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 60.3 kB (baseline 60.3 kB) • 🔴 +1 BReusable component library chunks
Status: 7 added / 7 removed / 6 unchanged Data & Services — 3.01 MB (baseline 3 MB) • 🔴 +9.1 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 344 kB (baseline 344 kB) • 🔴 +336 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 14 unchanged Vendor & Third-Party — 9.86 MB (baseline 9.86 MB) • 🔴 +23 BExternal libraries and shared vendor chunks
Status: 1 added / 1 removed / 15 unchanged Other — 8.57 MB (baseline 8.57 MB) • 🟢 -834 BBundles that do not match a named category
Status: 65 added / 65 removed / 70 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-16T04:42:28.525Z",
"gitSha": "581938314b17f585f3bb74c4b1700382bc2ec750",
"branch": "bl/subscription-success-80-20",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2020.5049999999858,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.155999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 366.124,
"heapDeltaBytes": 20346312,
"heapUsedBytes": 63859832,
"domNodes": 20,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 22.362999999999996,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2022.0299999999725,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.791000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 327.87100000000004,
"heapDeltaBytes": 19995512,
"heapUsedBytes": 63077908,
"domNodes": 16,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 16.022000000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-idle",
"durationMs": 2000.750000000039,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.889,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 396.141,
"heapDeltaBytes": 20594848,
"heapUsedBytes": 64586400,
"domNodes": 16,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 21.751,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2057.539999999989,
"styleRecalcs": 80,
"styleRecalcDurationMs": 45.583999999999996,
"layouts": 12,
"layoutDurationMs": 3.4070000000000005,
"taskDurationMs": 1027.0739999999998,
"heapDeltaBytes": 15663168,
"heapUsedBytes": 59016840,
"domNodes": 65,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 139.495,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2006.386999999961,
"styleRecalcs": 81,
"styleRecalcDurationMs": 44.173,
"layouts": 12,
"layoutDurationMs": 3.55,
"taskDurationMs": 955.52,
"heapDeltaBytes": 15750016,
"heapUsedBytes": 59265972,
"domNodes": 66,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 133.56099999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1829.1090000000167,
"styleRecalcs": 73,
"styleRecalcDurationMs": 42.93900000000001,
"layouts": 12,
"layoutDurationMs": 3.51,
"taskDurationMs": 803.02,
"heapDeltaBytes": 16006876,
"heapUsedBytes": 59285492,
"domNodes": 57,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 147.76500000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1738.7750000000324,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.553,
"layouts": 6,
"layoutDurationMs": 0.5429999999999998,
"taskDurationMs": 324.701,
"heapDeltaBytes": 24635136,
"heapUsedBytes": 68261104,
"domNodes": 78,
"jsHeapTotalBytes": 21495808,
"scriptDurationMs": 27.891,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1748.894000000007,
"styleRecalcs": 30,
"styleRecalcDurationMs": 17.893,
"layouts": 6,
"layoutDurationMs": 0.6309999999999998,
"taskDurationMs": 306.11299999999994,
"heapDeltaBytes": 24894080,
"heapUsedBytes": 68507312,
"domNodes": 78,
"jsHeapTotalBytes": 21495808,
"scriptDurationMs": 29.753999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1720.0520000000097,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.426,
"layouts": 6,
"layoutDurationMs": 0.6150000000000001,
"taskDurationMs": 338.812,
"heapDeltaBytes": 24471360,
"heapUsedBytes": 68171268,
"domNodes": 78,
"jsHeapTotalBytes": 21233664,
"scriptDurationMs": 33.07000000000001,
"eventListeners": 21,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 613.3450000000096,
"styleRecalcs": 8,
"styleRecalcDurationMs": 5.717,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 379.737,
"heapDeltaBytes": 6680544,
"heapUsedBytes": 51921020,
"domNodes": 12,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 67.95000000000002,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66666666666665,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 566.9750000000136,
"styleRecalcs": 13,
"styleRecalcDurationMs": 12.473999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 347.747,
"heapDeltaBytes": 6821132,
"heapUsedBytes": 50566424,
"domNodes": 22,
"jsHeapTotalBytes": 12845056,
"scriptDurationMs": 62.85500000000001,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 597.6010000000542,
"styleRecalcs": 13,
"styleRecalcDurationMs": 10.950000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 373.37000000000006,
"heapDeltaBytes": 6351680,
"heapUsedBytes": 49555964,
"domNodes": 22,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 68.81499999999998,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2009.7310000000164,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.468000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 528.675,
"heapDeltaBytes": 3786060,
"heapUsedBytes": 55937396,
"domNodes": -257,
"jsHeapTotalBytes": 16179200,
"scriptDurationMs": 97.08200000000001,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2039.3119999999954,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.941000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 621.2420000000001,
"heapDeltaBytes": 4294328,
"heapUsedBytes": 56382232,
"domNodes": -257,
"jsHeapTotalBytes": 16023552,
"scriptDurationMs": 114.71600000000001,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2063.6120000000346,
"styleRecalcs": 10,
"styleRecalcDurationMs": 11.456000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 559.1379999999999,
"heapDeltaBytes": 2849820,
"heapUsedBytes": 55666848,
"domNodes": -259,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 103.39600000000002,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2143.983999999989,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.764999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1090.994,
"heapDeltaBytes": 14753356,
"heapUsedBytes": 69253996,
"domNodes": -261,
"jsHeapTotalBytes": 17694720,
"scriptDurationMs": 394.45799999999997,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2135.865000000024,
"styleRecalcs": 68,
"styleRecalcDurationMs": 15.776999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1205.568,
"heapDeltaBytes": 17954064,
"heapUsedBytes": 72786708,
"domNodes": -262,
"jsHeapTotalBytes": 17170432,
"scriptDurationMs": 455.33299999999997,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2109.9859999999353,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.087,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1136.776,
"heapDeltaBytes": -9266116,
"heapUsedBytes": 54979620,
"domNodes": -259,
"jsHeapTotalBytes": 18071552,
"scriptDurationMs": 433.231,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3153.3509999999865,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.427,
"layouts": 60,
"layoutDurationMs": 7.564,
"taskDurationMs": 1334.4740000000002,
"heapDeltaBytes": 8390168,
"heapUsedBytes": 64407860,
"domNodes": -266,
"jsHeapTotalBytes": 15130624,
"scriptDurationMs": 488.247,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3180.9540000000425,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.151,
"layouts": 60,
"layoutDurationMs": 7.597999999999999,
"taskDurationMs": 1341.642,
"heapDeltaBytes": 6288072,
"heapUsedBytes": 62392584,
"domNodes": -266,
"jsHeapTotalBytes": 18014208,
"scriptDurationMs": 492.544,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3148.5099999999875,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.387000000000004,
"layouts": 60,
"layoutDurationMs": 7.384999999999999,
"taskDurationMs": 1340.009,
"heapDeltaBytes": 8297948,
"heapUsedBytes": 64360404,
"domNodes": -267,
"jsHeapTotalBytes": 15654912,
"scriptDurationMs": 495.03799999999995,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2040.8739999999739,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 585.756,
"heapDeltaBytes": 3302476,
"heapUsedBytes": 58801144,
"domNodes": -262,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 102.16200000000002,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2052.7700000000095,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.758,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 539.273,
"heapDeltaBytes": 2623496,
"heapUsedBytes": 58267312,
"domNodes": -262,
"jsHeapTotalBytes": 16441344,
"scriptDurationMs": 97.94299999999998,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2037.0040000000245,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.562999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 589.6839999999999,
"heapDeltaBytes": 4671216,
"heapUsedBytes": 58509724,
"domNodes": -259,
"jsHeapTotalBytes": 16441344,
"scriptDurationMs": 107.17099999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 574.485999999979,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.898,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 359.626,
"heapDeltaBytes": 6805844,
"heapUsedBytes": 50603104,
"domNodes": 20,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 129.504,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 574.544000000003,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.289,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 356.73199999999997,
"heapDeltaBytes": 7010836,
"heapUsedBytes": 51004644,
"domNodes": 20,
"jsHeapTotalBytes": 12320768,
"scriptDurationMs": 124.854,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 611.2749999999778,
"styleRecalcs": 46,
"styleRecalcDurationMs": 11.663999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 383.74499999999995,
"heapDeltaBytes": 6328012,
"heapUsedBytes": 49559860,
"domNodes": 18,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 129.998,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-idle",
"durationMs": 2000.6539999999973,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.411,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 367.777,
"heapDeltaBytes": 20229288,
"heapUsedBytes": 64000584,
"domNodes": 21,
"jsHeapTotalBytes": 23592960,
"scriptDurationMs": 21.391000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2020.4929999999877,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.772999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 342.37,
"heapDeltaBytes": 19920596,
"heapUsedBytes": 63722544,
"domNodes": 20,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 20.237,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2034.9029999999857,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.383,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 394.821,
"heapDeltaBytes": 19993028,
"heapUsedBytes": 64311584,
"domNodes": 18,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 19.123,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1972.9889999999841,
"styleRecalcs": 84,
"styleRecalcDurationMs": 45.875,
"layouts": 16,
"layoutDurationMs": 4.4990000000000006,
"taskDurationMs": 915.538,
"heapDeltaBytes": 11571672,
"heapUsedBytes": 55350192,
"domNodes": 72,
"jsHeapTotalBytes": 22282240,
"scriptDurationMs": 106.683,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1966.6119999999978,
"styleRecalcs": 83,
"styleRecalcDurationMs": 47.681000000000004,
"layouts": 16,
"layoutDurationMs": 5.054,
"taskDurationMs": 925.4299999999998,
"heapDeltaBytes": 12139632,
"heapUsedBytes": 56037160,
"domNodes": 70,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 103.88,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1973.779000000036,
"styleRecalcs": 84,
"styleRecalcDurationMs": 51.67400000000001,
"layouts": 16,
"layoutDurationMs": 4.732,
"taskDurationMs": 911.0150000000001,
"heapDeltaBytes": 11911076,
"heapUsedBytes": 55799664,
"domNodes": 72,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 106.83500000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8385.791000000041,
"styleRecalcs": 251,
"styleRecalcDurationMs": 44.291,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3777.9,
"heapDeltaBytes": 24882072,
"heapUsedBytes": 76973424,
"domNodes": -261,
"jsHeapTotalBytes": 19267584,
"scriptDurationMs": 1309.708,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8191.633999999965,
"styleRecalcs": 251,
"styleRecalcDurationMs": 43.882000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3654.7899999999995,
"heapDeltaBytes": 24071004,
"heapUsedBytes": 76213888,
"domNodes": -260,
"jsHeapTotalBytes": 19529728,
"scriptDurationMs": 1242.6870000000001,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999952,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8243.014000000017,
"styleRecalcs": 251,
"styleRecalcDurationMs": 46.782999999999994,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4148.297,
"heapDeltaBytes": 22029060,
"heapUsedBytes": 74881920,
"domNodes": -262,
"jsHeapTotalBytes": 19267584,
"scriptDurationMs": 1351.672,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12684.451000000025,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12671.208,
"heapDeltaBytes": -28548216,
"heapUsedBytes": 167987544,
"domNodes": -9850,
"jsHeapTotalBytes": 22110208,
"scriptDurationMs": 650.875,
"eventListeners": -23963,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12049.476000000028,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12040.119999999999,
"heapDeltaBytes": -46557800,
"heapUsedBytes": 169062532,
"domNodes": -9850,
"jsHeapTotalBytes": 24731648,
"scriptDurationMs": 575.407,
"eventListeners": -23959,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12316.237999999998,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12288.041,
"heapDeltaBytes": -46571432,
"heapUsedBytes": 168840712,
"domNodes": -9850,
"jsHeapTotalBytes": 25518080,
"scriptDurationMs": 629.101,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14336.066000000017,
"styleRecalcs": 67,
"styleRecalcDurationMs": 13.805999999999985,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14311.06,
"heapDeltaBytes": -38397236,
"heapUsedBytes": 174033148,
"domNodes": -9850,
"jsHeapTotalBytes": -24375296,
"scriptDurationMs": 876.0870000000001,
"eventListeners": -23955,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14017.435999999976,
"styleRecalcs": 64,
"styleRecalcDurationMs": 13.160999999999978,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13979.930999999999,
"heapDeltaBytes": -32399144,
"heapUsedBytes": 177871780,
"domNodes": -9850,
"jsHeapTotalBytes": 25432064,
"scriptDurationMs": 827.486,
"eventListeners": -23955,
"totalBlockingTimeMs": 1,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15055.287000000022,
"styleRecalcs": 75,
"styleRecalcDurationMs": 16.56400000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15023.491999999998,
"heapDeltaBytes": -37663760,
"heapUsedBytes": 174111728,
"domNodes": -9850,
"jsHeapTotalBytes": -26996736,
"scriptDurationMs": 977.372,
"eventListeners": -23953,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "workflow-execution",
"durationMs": 491.97800000001735,
"styleRecalcs": 15,
"styleRecalcDurationMs": 23.505,
"layouts": 5,
"layoutDurationMs": 1.583,
"taskDurationMs": 137.18899999999996,
"heapDeltaBytes": 5362644,
"heapUsedBytes": 50828632,
"domNodes": 165,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 29.823000000000004,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 454.65599999999995,
"styleRecalcs": 14,
"styleRecalcDurationMs": 20.081,
"layouts": 4,
"layoutDurationMs": 1.0909999999999997,
"taskDurationMs": 115.80499999999996,
"heapDeltaBytes": 4970596,
"heapUsedBytes": 50656656,
"domNodes": 150,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 25.436000000000003,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 462.2580000000198,
"styleRecalcs": 15,
"styleRecalcDurationMs": 21.540999999999997,
"layouts": 5,
"layoutDurationMs": 1.3719999999999997,
"taskDurationMs": 119.79000000000003,
"heapDeltaBytes": 5125764,
"heapUsedBytes": 50543816,
"domNodes": 154,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 29.12100000000001,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/platform/telemetry/types.ts (1)
361-371: AddSubscriptionSuccessMetadatatoTelemetryEventPropertiestoo.The new payload is now passed through provider queue helpers typed as
TelemetryEventProperties, but that union still omitsSubscriptionSuccessMetadata. Today this works only becauseSubscriptionMetadatais structurally permissive, so we lose the intended type check on queued analytics payloads.♻️ Suggested follow-up
export type TelemetryEventProperties = | AuthMetadata | SurveyResponses | TemplateMetadata | ExecutionContext | RunButtonProperties | ExecutionErrorMetadata | ExecutionSuccessMetadata | CreditTopupMetadata | WorkflowImportMetadata | TemplateLibraryMetadata | TemplateLibraryClosedMetadata | PageVisibilityMetadata | TabCountMetadata | NodeSearchMetadata | NodeSearchResultMetadata | TemplateFilterMetadata | SettingChangedMetadata | UiButtonClickMetadata | HelpCenterOpenedMetadata | HelpResourceClickedMetadata | HelpCenterClosedMetadata | WorkflowCreatedMetadata | EnterLinearMetadata | ShareFlowMetadata | WorkflowSavedMetadata | DefaultViewSetMetadata - | SubscriptionMetadata + | SubscriptionMetadata + | SubscriptionSuccessMetadataAs per coding guidelines, "Use TypeScript for type safety".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/telemetry/types.ts` around lines 361 - 371, TelemetryEventProperties currently omits the new SubscriptionSuccessMetadata type so queued provider helpers typed as TelemetryEventProperties lose the intended type checking; update the TelemetryEventProperties union to include SubscriptionSuccessMetadata (alongside the existing SubscriptionMetadata and other variants) so payloads like subscription success events are properly typed when passed through the provider queue helpers; locate the TelemetryEventProperties type and add SubscriptionSuccessMetadata to its union members to restore strict typing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/cloud/subscription/components/PricingTable.vue`:
- Around line 483-491: The pending-checkout write is happening before
accessBillingPortal(checkoutTier) succeeds; move the call to
recordPendingSubscriptionCheckoutAttempt so it only runs after the portal open
is confirmed (i.e., after await accessBillingPortal(checkoutTier) resolves and
you have the portal URL or success). Specifically, remove or postpone the
existing recordPendingSubscriptionCheckoutAttempt(...) invocation located
alongside the checkout initiation and instead invoke it in the success branch
that runs once accessBillingPortal returns successfully (use the same payload:
tier: tierKey, cycle: currentBillingCycle.value, checkout_type: 'change',
previous_tier: currentTierKey.value when present, and previous_cycle based on
isYearlySubscription.value) so stale entries aren't written when
accessBillingPortal fails.
In `@src/platform/cloud/subscription/composables/useSubscription.ts`:
- Around line 343-350: In handlePendingSubscriptionCheckoutChange, clear any
existing pending retry timer before resetting pendingCheckoutRecoveryAttempt and
starting recovery so the new attempt doesn't inherit a stale delay;
specifically, if pendingCheckoutRecoveryTimeout is set, call
clearTimeout(pendingCheckoutRecoveryTimeout) (or the appropriate clear function)
and null it out prior to setting pendingCheckoutRecoveryAttempt = 0 and invoking
recoverPendingSubscriptionCheckout('retry'); ensure this logic complements or
replaces stopPendingCheckoutRecovery() usage so timers are consistently cleared.
In `@src/platform/cloud/subscription/utils/subscriptionCheckoutTracker.ts`:
- Around line 69-81: The getStorage/localStorage access can throw in restricted
contexts; update getStorage() and all places that call it (notably
recordPendingSubscriptionCheckoutAttempt and
getPendingSubscriptionCheckoutAttempt, plus the blocks around lines ~169-176,
186-205, 239-243) to "fail open": wrap any globalThis.localStorage access and
calls to getItem/setItem/removeItem in try/catch and treat exceptions as
storage-unavailable (return null or no-op) rather than re-throwing; ensure
callers handle a null storage gracefully (skip persistence, return
undefined/defaults) so checkout initiation and recovery continue even when the
storage API is blocked.
In `@src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts`:
- Around line 118-122: The call to recordPendingSubscriptionCheckoutAttempt in
subscriptionCheckoutUtil.ts is executed before the checkout navigation is
actually initiated; move the call so it only runs after the browser
navigation/tab creation is confirmed (i.e., after the window.open or the
redirect code path successfully returns a non-null Window or after
location.assign/location.href is executed). Update the checkout flow in the
function where recordPendingSubscriptionCheckoutAttempt is called (refer to the
same function name and the branch that handles window.open / redirect
initiation) to record the pending attempt only inside the success branch,
leaving the original error/fallback branches untouched; keep
subscriptionCheckoutTracker.ts behavior unchanged so that later successful
checkouts still clear the recorded attempt. Ensure you handle both popup
(window.open returning a Window object) and redirect flows consistently.
In `@src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts`:
- Around line 171-181: trackMonthlySubscriptionSucceeded clears ecommerce state
unconditionally; wrap the window.dataLayer?.push({ ecommerce: null }) call with
the same GTM-initialization guard used by pushEvent so ecommerce is only reset
when GTM is initialized. Locate trackMonthlySubscriptionSucceeded and apply the
same boolean/method check (the one pushEvent uses to bail out when GTM isn't
present, e.g., this.gtmInitialized / this.isGtmInitialized()) around the
dataLayer push so GA-only setups won't clear ecommerce without sending
subscription_success.
---
Nitpick comments:
In `@src/platform/telemetry/types.ts`:
- Around line 361-371: TelemetryEventProperties currently omits the new
SubscriptionSuccessMetadata type so queued provider helpers typed as
TelemetryEventProperties lose the intended type checking; update the
TelemetryEventProperties union to include SubscriptionSuccessMetadata (alongside
the existing SubscriptionMetadata and other variants) so payloads like
subscription success events are properly typed when passed through the provider
queue helpers; locate the TelemetryEventProperties type and add
SubscriptionSuccessMetadata to its union members to restore strict typing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9f8ced6d-7f40-4525-82d8-b3bdd38afcf7
📒 Files selected for processing (12)
src/platform/cloud/subscription/components/PricingTable.vuesrc/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vuesrc/platform/cloud/subscription/composables/useSubscription.test.tssrc/platform/cloud/subscription/composables/useSubscription.tssrc/platform/cloud/subscription/utils/subscriptionCheckoutTracker.tssrc/platform/cloud/subscription/utils/subscriptionCheckoutUtil.tssrc/platform/telemetry/TelemetryRegistry.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.tssrc/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.tssrc/platform/telemetry/types.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e5c8d26e56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Also addressed the unthreaded typing nit from the CodeRabbit summary: |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/composables/auth/useAuthActions.ts (1)
118-136:⚠️ Potential issue | 🟠 MajorHandle the new
falseresult in all billing-portal flows.This wrapper now distinguishes “popup blocked” from success, but
manageSubscription()insrc/platform/cloud/subscription/composables/useSubscription.tsstill ignores the return value and startsstartCancellationWatcher()unconditionally. That leaves one portal flow behaving as if the portal opened when it did not. Either make blocked-popup failure user-visible here (toast/throw) or update the remaining callers to gate follow-up work on the returned boolean.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/composables/auth/useAuthActions.ts` around lines 118 - 136, The accessBillingPortal wrapper (accessBillingPortal) can now return false when window.open is blocked, but callers like manageSubscription in useSubscription.ts still ignore this result and unconditionally call startCancellationWatcher; update the flow so blocked-popup failure is handled: either have accessBillingPortal throw or show a toast when it returns false, or change callers (e.g., manageSubscription) to await the boolean and only call startCancellationWatcher when it is true; modify the manageSubscription function to check the accessBillingPortal() result before invoking startCancellationWatcher (or propagate the error from accessBillingPortal) so follow-up work only runs on actual success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/cloud/subscription/components/PricingTable.vue`:
- Around line 483-496: Before calling await accessBillingPortal(checkoutTier)
capture the plan snapshot (e.g., const openedCycle = currentBillingCycle.value
and const openedIsYearly = isYearlySubscription.value and const openedTier =
tierKey or use checkoutTier) so the state cannot drift while the portal request
is in flight; then after didOpenPortal === true call
recordPendingSubscriptionCheckoutAttempt using those captured values (use
openedCycle / openedIsYearly / openedTier and currentTierKey.value as before)
instead of rereading currentBillingCycle.value or isYearlySubscription.value so
the persisted snapshot matches the portal that was actually opened.
In `@src/platform/cloud/subscription/composables/useSubscription.ts`:
- Around line 371-375: The visibilitychange listener is currently attached to
defaultWindow which never receives that document-dispatched event; change the
listener target to the document (e.g., defaultDocument or document) in the
useEventListener call so recoverPendingSubscriptionCheckout('visibilitychange')
runs when the tab becomes visible; update the call site where
useEventListener(defaultWindow, 'visibilitychange', ...) is used and keep the
same handler referencing recoverPendingSubscriptionCheckout.
---
Outside diff comments:
In `@src/composables/auth/useAuthActions.ts`:
- Around line 118-136: The accessBillingPortal wrapper (accessBillingPortal) can
now return false when window.open is blocked, but callers like
manageSubscription in useSubscription.ts still ignore this result and
unconditionally call startCancellationWatcher; update the flow so blocked-popup
failure is handled: either have accessBillingPortal throw or show a toast when
it returns false, or change callers (e.g., manageSubscription) to await the
boolean and only call startCancellationWatcher when it is true; modify the
manageSubscription function to check the accessBillingPortal() result before
invoking startCancellationWatcher (or propagate the error from
accessBillingPortal) so follow-up work only runs on actual success.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1d392b4b-3e4b-4764-93db-1188706f1806
📒 Files selected for processing (12)
src/composables/auth/useAuthActions.tssrc/platform/cloud/subscription/components/PricingTable.test.tssrc/platform/cloud/subscription/components/PricingTable.vuesrc/platform/cloud/subscription/composables/useSubscription.test.tssrc/platform/cloud/subscription/composables/useSubscription.tssrc/platform/cloud/subscription/utils/subscriptionCheckoutTracker.test.tssrc/platform/cloud/subscription/utils/subscriptionCheckoutTracker.tssrc/platform/cloud/subscription/utils/subscriptionCheckoutUtil.test.tssrc/platform/cloud/subscription/utils/subscriptionCheckoutUtil.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.tssrc/platform/telemetry/types.ts
✅ Files skipped from review due to trivial changes (1)
- src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts
- src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
- src/platform/cloud/subscription/utils/subscriptionCheckoutTracker.ts
|
Also addressed the outside-diff review note: |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/platform/cloud/subscription/composables/useSubscription.test.ts (1)
539-574: Visibility-triggered recovery test relies on happy-dom default.The test dispatches
visibilitychangeondocumentwithout mockingvisibilityState. This works because happy-dom's default is'visible', which passes the implementation's condition check. Consider adding a comment noting this assumption for future maintainers.📝 Add clarifying comment
) + // Note: happy-dom's document.visibilityState defaults to 'visible', + // so the implementation's visibility check passes without explicit mocking document.dispatchEvent(new Event('visibilitychange'))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/cloud/subscription/composables/useSubscription.test.ts` around lines 539 - 574, The test rechecks pending checkout on document visibility but relies on happy-dom's default document.visibilityState === 'visible'; add a clarifying inline comment right before the document.dispatchEvent(new Event('visibilitychange')) call in the it('rechecks pending checkout attempts when the document becomes visible'...) test (and mention the dependency on happy-dom's default) so future maintainers understand why visibilityState is not explicitly mocked; reference the test helper/use hook useSubscriptionWithScope and the PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY setup to indicate the context of the visibility dispatch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/platform/cloud/subscription/composables/useSubscription.test.ts`:
- Around line 539-574: The test rechecks pending checkout on document visibility
but relies on happy-dom's default document.visibilityState === 'visible'; add a
clarifying inline comment right before the document.dispatchEvent(new
Event('visibilitychange')) call in the it('rechecks pending checkout attempts
when the document becomes visible'...) test (and mention the dependency on
happy-dom's default) so future maintainers understand why visibilityState is not
explicitly mocked; reference the test helper/use hook useSubscriptionWithScope
and the PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY setup to indicate the context
of the visibility dispatch.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 14c27c93-ddf3-4406-8100-cd1920c7f952
📒 Files selected for processing (4)
src/platform/cloud/subscription/components/PricingTable.test.tssrc/platform/cloud/subscription/components/PricingTable.vuesrc/platform/cloud/subscription/composables/useSubscription.test.tssrc/platform/cloud/subscription/composables/useSubscription.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/platform/cloud/subscription/components/PricingTable.vue
- src/platform/cloud/subscription/components/PricingTable.test.ts
AustinMroz
left a comment
There was a problem hiding this comment.
The proposed direction seems reasonable and has my support.
The PR itself was rather difficult to read. It's long and not a section of the code base I'm well acquainted with, but I didn't see anything wrong, let along blocking.
| if (!storage) { | ||
| return { | ||
| attempt_id: createAttemptId(), | ||
| started_at_ms: Date.now(), | ||
| tier: input.tier, | ||
| cycle: input.cycle, | ||
| checkout_type: input.checkout_type, | ||
| ...(input.previous_tier ? { previous_tier: input.previous_tier } : {}), | ||
| ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) | ||
| } | ||
| } | ||
|
|
||
| const attempt: PendingSubscriptionCheckoutAttempt = { | ||
| attempt_id: createAttemptId(), | ||
| started_at_ms: Date.now(), | ||
| tier: input.tier, | ||
| cycle: input.cycle, | ||
| checkout_type: input.checkout_type, | ||
| ...(input.previous_tier ? { previous_tier: input.previous_tier } : {}), | ||
| ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) | ||
| } | ||
|
|
||
| try { | ||
| storage.setItem( | ||
| PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY, | ||
| JSON.stringify(attempt) | ||
| ) | ||
| } catch { | ||
| return attempt | ||
| } | ||
| dispatchPendingCheckoutChangeEvent() | ||
|
|
||
| return attempt |
There was a problem hiding this comment.
I'd rather we didn't have the object creation described twice here.
Maybe:
| if (!storage) { | |
| return { | |
| attempt_id: createAttemptId(), | |
| started_at_ms: Date.now(), | |
| tier: input.tier, | |
| cycle: input.cycle, | |
| checkout_type: input.checkout_type, | |
| ...(input.previous_tier ? { previous_tier: input.previous_tier } : {}), | |
| ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) | |
| } | |
| } | |
| const attempt: PendingSubscriptionCheckoutAttempt = { | |
| attempt_id: createAttemptId(), | |
| started_at_ms: Date.now(), | |
| tier: input.tier, | |
| cycle: input.cycle, | |
| checkout_type: input.checkout_type, | |
| ...(input.previous_tier ? { previous_tier: input.previous_tier } : {}), | |
| ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) | |
| } | |
| try { | |
| storage.setItem( | |
| PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY, | |
| JSON.stringify(attempt) | |
| ) | |
| } catch { | |
| return attempt | |
| } | |
| dispatchPendingCheckoutChangeEvent() | |
| return attempt | |
| const attempt: PendingSubscriptionCheckoutAttempt = { | |
| attempt_id: createAttemptId(), | |
| started_at_ms: Date.now(), | |
| tier: input.tier, | |
| cycle: input.cycle, | |
| checkout_type: input.checkout_type, | |
| ...(input.previous_tier ? { previous_tier: input.previous_tier } : {}), | |
| ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) | |
| } | |
| if (!storage) return attempt | |
| try { | |
| storage.setItem( | |
| PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY, | |
| JSON.stringify(attempt) | |
| ) | |
| dispatchPendingCheckoutChangeEvent() | |
| } catch { } | |
| return attempt |
There was a problem hiding this comment.
Agreed that this is valid as a follow-up cleanup, but non-blocking for this PR. I’ve tracked it in Linear as FE-209 and assigned it to myself.
There was a problem hiding this comment.
It was so small I didn't even notice! I should've added it to this PR
|
|
Improving our subscription detection system. Optimal will have to come after BE team brings personal billing to cloud repo off of comfy api. - replace the dialog-local focus poller with a frontend checkout tracker stored in `localStorage` - recover pending subscription checkouts from app boot plus global page lifecycle (`pageshow`, `visibilitychange`) with bounded retries only while an attempt is pending - emit `subscription_success` through GTM with frontend-derived metadata once subscription state reaches the expected target tier/cycle This is the frontend-only 80/20 path. It fixes the brittle "old tab must regain focus" behavior without adding new backend endpoints or backend event storage. The browser records one pending checkout attempt when checkout is opened, and any returning cloud tab can recover it later by comparing current subscription state against the expected target plan. - browser-scoped, not backend-authoritative - no server transaction id - scheduled downgrades through the billing portal are intentionally not inferred as immediate success events - still best-effort compared with the backend outbox/WebSocket approach - `pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts` - `pnpm typecheck` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11286-feat-add-frontend-subscription-success-recovery-3436d73d3650814d9f74c89e6926aa84) by [Unito](https://www.unito.io)
#11476) Backports #11286 ## Summary - backport frontend subscription success recovery to `cloud/1.43` ## Backport Delta - `18ef350db` is the cherry-pick of main's `ecb7fd479` from #11286 - the backport-only review surface is intentionally small: - `src/platform/cloud/subscription/components/PricingTable.test.ts`: adapt the assertions to `cloud/1.43`'s existing `mount`-based test harness - `src/platform/cloud/subscription/constants/tierPricing.ts`: one-line type-only export (`type SubscriptionTier` -> `export type SubscriptionTier`) - runtime review should therefore mostly follow #11286 rather than treating this as a novel branch-specific implementation ## Why The automated backport failed because `cloud/1.43` had drifted in `PricingTable.test.ts`. The runtime code itself cherry-picked cleanly; the only non-test branch-local change was the small `SubscriptionTier` export needed to match the API surface expected by the backported code. ## Validation - `pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/cloud/subscription/components/PricingTable.test.ts src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts` - `pnpm typecheck` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11476-backport-cloud-1-43-feat-add-frontend-subscription-success-recovery-3486d73d3650813ebc87c162a8aa759e) by [Unito](https://www.unito.io)
Improving our subscription detection system. Optimal will have to come after BE team brings personal billing to cloud repo off of comfy api. ## Summary - replace the dialog-local focus poller with a frontend checkout tracker stored in `localStorage` - recover pending subscription checkouts from app boot plus global page lifecycle (`pageshow`, `visibilitychange`) with bounded retries only while an attempt is pending - emit `subscription_success` through GTM with frontend-derived metadata once subscription state reaches the expected target tier/cycle ## Why This is the frontend-only 80/20 path. It fixes the brittle "old tab must regain focus" behavior without adding new backend endpoints or backend event storage. The browser records one pending checkout attempt when checkout is opened, and any returning cloud tab can recover it later by comparing current subscription state against the expected target plan. ## Tradeoffs - browser-scoped, not backend-authoritative - no server transaction id - scheduled downgrades through the billing portal are intentionally not inferred as immediate success events - still best-effort compared with the backend outbox/WebSocket approach ## Validation - `pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts` - `pnpm typecheck` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11286-feat-add-frontend-subscription-success-recovery-3436d73d3650814d9f74c89e6926aa84) by [Unito](https://www.unito.io)
Improving our subscription detection system. Optimal will have to come after BE team brings personal billing to cloud repo off of comfy api. ## Summary - replace the dialog-local focus poller with a frontend checkout tracker stored in `localStorage` - recover pending subscription checkouts from app boot plus global page lifecycle (`pageshow`, `visibilitychange`) with bounded retries only while an attempt is pending - emit `subscription_success` through GTM with frontend-derived metadata once subscription state reaches the expected target tier/cycle ## Why This is the frontend-only 80/20 path. It fixes the brittle "old tab must regain focus" behavior without adding new backend endpoints or backend event storage. The browser records one pending checkout attempt when checkout is opened, and any returning cloud tab can recover it later by comparing current subscription state against the expected target plan. ## Tradeoffs - browser-scoped, not backend-authoritative - no server transaction id - scheduled downgrades through the billing portal are intentionally not inferred as immediate success events - still best-effort compared with the backend outbox/WebSocket approach ## Validation - `pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts` - `pnpm typecheck` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11286-feat-add-frontend-subscription-success-recovery-3436d73d3650814d9f74c89e6926aa84) by [Unito](https://www.unito.io)
Improving our subscription detection system. Optimal will have to come after BE team brings personal billing to cloud repo off of comfy api. ## Summary - replace the dialog-local focus poller with a frontend checkout tracker stored in `localStorage` - recover pending subscription checkouts from app boot plus global page lifecycle (`pageshow`, `visibilitychange`) with bounded retries only while an attempt is pending - emit `subscription_success` through GTM with frontend-derived metadata once subscription state reaches the expected target tier/cycle ## Why This is the frontend-only 80/20 path. It fixes the brittle "old tab must regain focus" behavior without adding new backend endpoints or backend event storage. The browser records one pending checkout attempt when checkout is opened, and any returning cloud tab can recover it later by comparing current subscription state against the expected target plan. ## Tradeoffs - browser-scoped, not backend-authoritative - no server transaction id - scheduled downgrades through the billing portal are intentionally not inferred as immediate success events - still best-effort compared with the backend outbox/WebSocket approach ## Validation - `pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts` - `pnpm typecheck` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11286-feat-add-frontend-subscription-success-recovery-3436d73d3650814d9f74c89e6926aa84) by [Unito](https://www.unito.io)
Improving our subscription detection system. Optimal will have to come after BE team brings personal billing to cloud repo off of comfy api.
Summary
localStoragepageshow,visibilitychange) with bounded retries only while an attempt is pendingsubscription_successthrough GTM with frontend-derived metadata once subscription state reaches the expected target tier/cycleWhy
This is the frontend-only 80/20 path. It fixes the brittle "old tab must regain focus" behavior without adding new backend endpoints or backend event storage. The browser records one pending checkout attempt when checkout is opened, and any returning cloud tab can recover it later by comparing current subscription state against the expected target plan.
Tradeoffs
Validation
pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.tspnpm typecheck┆Issue is synchronized with this Notion page by Unito